use crate::db::{delete, insert, UserSub, SUBSCRIBE};
use crate::domain::user::User;
use dashmap::DashMap;
use libnftp::auth::{Authenticator, Credentials};
use regex::Regex;
use rusqlite::{Error, ErrorCode};
use salvo::prelude::*;
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use tokio::sync::{broadcast, mpsc};
use tracing::info;

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SubRequest {
    watch_path: String,
    receive_url: Vec<String>,
}

#[derive(Serialize)]
struct SubResponse {
    code: i32,
    message: String,
}

async fn valid_user(authenticator: Arc<dyn Authenticator<User> + Send + Sync>, req: &mut Request, res: &mut Response) -> Option<String> {
    let ftp_user = req.header::<String>("NFTP-User");
    let ftp_password = req.header::<String>("NFTP-Password");
    match (ftp_user, ftp_password) {
        (Some(ftp_user), Some(ftp_password)) => {
            if let Err(_) = authenticator.authenticate(ftp_user.as_str(), &Credentials::from(ftp_password.as_str())).await {
                res.status_code(StatusCode::BAD_REQUEST);
                res.render(Json(
                    SubResponse {
                        code: 1,
                        message: "Invalid user or password".into(),
                    }
                ));
                None
            } else {
                Some(ftp_user)
            }
        },
        _ => {
            res.status_code(StatusCode::BAD_REQUEST);
            res.render(Json(
                SubResponse {
                    code: 1,
                    message: "Invalid user or password".into(),
                }
            ));
            None
        }
    }
}

struct Subscribe {
    root_dir: String,
    authenticator: Arc<dyn Authenticator<User> + Send + Sync>,
    path_regex: Regex,
    url_regex: Regex,
}

#[handler]
impl Subscribe {
    async fn handle(&self, req: &mut Request, res: &mut Response) {
        let Some(ftp_user) = valid_user(self.authenticator.clone(), req, res).await else {
            return;
        };

        let Ok(sub_req) = req.parse_json::<SubRequest>().await
        else {
            res.status_code(StatusCode::BAD_REQUEST);
            res.render(Json(
                SubResponse {
                    code: 1,
                    message: "Invalid json data".into(),
                }
            ));
            return;
        };

        if !self.path_regex.is_match(&sub_req.watch_path) {
            res.status_code(StatusCode::BAD_REQUEST);
            res.render(Json(
                SubResponse {
                    code: 1,
                    message: format!("Invalid watch path {}", sub_req.watch_path),
                }
            ));
            return;
        }

        for url in &sub_req.receive_url {
            if !self.url_regex.is_match(url) {
                res.status_code(StatusCode::BAD_REQUEST);
                res.render(Json(
                    SubResponse {
                        code: 1,
                        message: format!("Invalid receive url {}", url),
                    }
                ));
                return;
            }
        }

        match insert(&self.root_dir, ftp_user, sub_req.watch_path, sub_req.receive_url) {
            Ok(message) => {
                info!("{}", &message);
                res.status_code(StatusCode::OK);
                res.render(Json(
                    SubResponse {
                        code: 0,
                        message,
                    }
                ));
            },
            Err(e) => {
                res.status_code(StatusCode::BAD_REQUEST);
                let message = match e {
                    Error::SqliteFailure(error, _) => {
                        if error.code == ErrorCode::ConstraintViolation {
                            "Duplicate watch path".into()
                        } else {
                            "NFTP internal exception".into()
                        }
                    },
                    Error::InvalidParameterName(name) => name,
                    _ => "NFTP internal exception".into()
                };
                res.render(Json(
                    SubResponse {
                        code: 1,
                        message,
                    }
                ))
            }
        }
    }
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct UnsubRequest {
    watch_path: String,
}

struct Unsubscribe {
    root_dir: String,
    authenticator: Arc<dyn Authenticator<User> + Send + Sync>,
}

#[handler]
impl Unsubscribe {
    async fn handle(&self, req: &mut Request, res: &mut Response) {
        let Some(ftp_user) = valid_user(self.authenticator.clone(), req, res).await else {
            return;
        };

        let Ok(unsub_req) = req.parse_json::<UnsubRequest>().await
        else {
            res.status_code(StatusCode::BAD_REQUEST);
            res.render(Json(
                SubResponse {
                    code: 1,
                    message: "Invalid json data".into(),
                }
            ));
            return;
        };

        match delete(&self.root_dir, &ftp_user, &unsub_req.watch_path) {
            Ok(message) => {
                info!("{}", &message);
                res.status_code(StatusCode::OK);
                res.render(Json(
                    SubResponse {
                        code: 0,
                        message,
                    }
                ));
            },
            Err(e) => {
                res.status_code(StatusCode::BAD_REQUEST);
                let message = match e {
                    Error::SqliteFailure(_, e) => e.unwrap_or("sql error".into()),
                    Error::InvalidParameterName(name) => name,
                    Error::QueryReturnedNoRows => format!("{} isn't subscribed by {}.", &unsub_req.watch_path, &ftp_user),
                    _ => "NFTP internal exception".into()
                };
                res.render(Json(
                    SubResponse {
                        code: 1,
                        message,
                    }
                ))
            }
        }
    }
}

struct SubscribeInfo {
    subscribe: &'static DashMap<String, UserSub>,
    authenticator: Arc<dyn Authenticator<User> + Send + Sync>,
}

#[handler]
impl SubscribeInfo {
    async fn handle(&self, req: &mut Request, res: &mut Response) {
        let Some(ftp_user) = valid_user(self.authenticator.clone(), req, res).await else {
            return;
        };
        let Some(entry) = self.subscribe.get(&ftp_user) else {
            res.status_code(StatusCode::OK);
            let _ = res.write_body("{}");
            return;
        };

        let info = &entry.sub;
        res.status_code(StatusCode::OK);
        res.render(Json(info));
    }
}

pub async fn start(
    root_dir: String,
    authenticator: Arc<dyn Authenticator<User> + Send + Sync>,
    bind_addr: String,
    ftp_addr: SocketAddr,
    mut shutdown: broadcast::Receiver<()>,
    done: mpsc::Sender<()>,
) -> Result<(), String> {
    let router = Router::with_path("nftp")
        .push(
            Router::with_path("status")
                .get(StatusHandler {ftp_addr})
        )
        .push(
            Router::with_path("subscribe")
                .post(Subscribe {
                    root_dir: root_dir.clone(),
                    authenticator: authenticator.clone(),
                    path_regex: Regex::new(r"^(/\w+)+$").unwrap(),
                    url_regex: Regex::new(r"^https?://\S{9,}$").unwrap(),
                })
        )
        .push(
            Router::with_path("subscribe")
                .get(SubscribeInfo {
                    subscribe: SUBSCRIBE.get().unwrap(),
                    authenticator: authenticator.clone(),
                })
        )
        .push(
            Router::with_path("unsubscribe")
                .post(Unsubscribe {
                    root_dir,
                    authenticator,
                })
        );

    let acceptor = match TcpListener::new(&bind_addr).try_bind().await {
        Ok(acceptor) => acceptor,
        Err(e) => {
            eprintln!("Start http at {bind_addr} error, reason: {e:?}");
            std::process::exit(1)
        },
    };
    let server = Server::new(acceptor);

    let handle = server.handle();
    tokio::spawn(async move {
        shutdown.recv().await.ok();
        handle.stop_graceful(Duration::from_secs(6));
    });

    server.serve(router).await;

    drop(done);
    Ok(())
}

#[derive(Serialize)]
struct Status {
    status: String,
}

struct StatusHandler {
    pub ftp_addr: SocketAddr,
}

#[handler]
impl StatusHandler {
    async fn handle(&self, res: &mut Response) {
        match self.ftp_probe().await {
            Ok(_) => {
                res.status_code(StatusCode::OK);
                res.render(Json(
                    Status {
                        status: "OK!".into()
                    }
                ));
            }
            Err(_e) => {
                res.status_code(StatusCode::OK);
                res.render(Json(
                    Status {
                        status: "Service unavailable!".into()
                    }
                ));
            }
        }
    }

    async fn ftp_probe(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let connect_to_addr = if self.ftp_addr.ip().is_unspecified() {
            SocketAddr::new(
                IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
                self.ftp_addr.port(),
            )
        } else {
            self.ftp_addr
        };

        let connection = tokio::net::TcpStream::connect(connect_to_addr).await?;
        let (rx, mut tx) = tokio::io::split(connection);
        let mut reader = tokio::io::BufReader::new(rx);

        // Consume welcome message
        let mut line_buf = String::new();
        reader.read_line(&mut line_buf).await?;

        tx.write_all(b"NOOP\r\n").await?;
        line_buf.clear();
        reader.read_line(&mut line_buf).await?;

        tx.write_all(b"QUIT\r\n").await?;
        line_buf.clear();
        reader.read_line(&mut line_buf).await?;

        Ok(())
    }
}
